#!python3.3
# -*- coding: utf-8 -*-
"""
last.fm module for NoiseBot
Requires pylast - https://code.google.com/p/pylast/
Applying the patch from issue #63 is required for LastFMUser.reload_loved()! - https://code.google.com/p/pylast/issues/detail?id=63
"""
import os
import sys
import logging
import datetime
import pickle
import glob
import traceback
import threading
import queue
import time
import types
import hashlib
import pprint

import pynoisebot

import pylast

class NotRegistered(Exception):
	pass

class _InvalidArguments(Exception):
	# used for internal functions and is NOT meant to be propagated up to process_command
	pass

class LastFMErrorEncountered(Exception):
	"""
	handle_lastfm_user_obj_errors generates and sends error messages; this is intended to propagate further up to the generic command call.
	exc is the exception that was originally raised.
	"""
	def __init__(self, exc):
		self.exc = exc

def handle_lastfm_user_obj_errors(lastfm_user_obj, method, message_namespace, *args, **kwargs):
	"""
	Wrapper for LastFMUser methods that handles errors.
	This is intended for handling errors related to operations for users, which are generally user-initiated.
	"""
	try:
		return method(lastfm_user_obj, *args, **kwargs)
	except pylast.WSError as exc:
		error_code = int(exc.get_id())
		if error_code == 2:
			lastfm_logger.exception('This service does not exist:')
		elif error_code == 4:
			lastfm_logger.warning('Could not authenticate: wrong username and/or password.')
		elif error_code == 6:
			try:
				error_message = 'The username {} does not exist on Last.fm.'.format(lastfm_user_obj.lastfm_username)
			except AttributeError:
				try:
					error_message = 'The username {} does not exist on Last.fm.'.format(kwargs['lastfm_username'])
				except KeyError:
					error_message = 'That username does not exist on Last.fm.'
			lastfm_logger.warning(error_message)
			if message_namespace is not None:
				send_notice(message_namespace.config_network_name, message_namespace.nick, error_message)
		elif error_code == 8:
			lastfm_logger.warning('Error fetching recent tracks for {}.'.format(lastfm_user_obj.lastfm_username))
			if message_namespace is not None:
				send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Could not fetch recent tracks. Try again.')
		elif error_code == 10:
			error_message = 'API key is invalid.'
			lastfm_logger.critical(error_message)
			die(error_message, exc)
		elif error_code == 11:
			error_message = 'Last.fm web services appear to be down.'
			lastfm_logger.warning(error_message)
			if message_namespace is not None:
				send_privmsg(message_namespace.config_network_name, message_namespace.source, error_message)
		elif error_code == 13:
			lastfm_logger.warning('Could not authenticate: bad API secret.')
		elif error_code == 26:
			error_message = 'API key has been suspended by Last.fm.'
			lastfm_logger.critical(error_message)
			die(error_message, exc)
		else:
			error_message = 'Other pylast error - Error {0} ({1}) (Network: {2})'.format(exc.status, exc.details, exc.network)
			lastfm_logger.warning(error_message)
		raise LastFMErrorEncountered(exc)
	except pylast.MalformedResponseError as exc:
		lastfm_logger.exception('Got a MalformedResponseError:')
		if message_namespace is not None:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Last.fm sent a malformed response and/or may be down. Try again.')
		raise LastFMErrorEncountered(exc)
	except pylast.NetworkError as exc:
		lastfm_logger.exception('Got a NetworkError:')
		if message_namespace is not None:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Could not connect to Last.fm.')
		raise LastFMErrorEncountered(exc)
	except NotRegistered:
		if message_namespace is not None:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Data cannot be saved for unregistered usernames.')
		raise LastFMErrorEncountered(exc)

def handle_background_lastfm_errors(func, desc, *args, **kwargs):
	"""
	Wrapper for background Last.fm API calls (e.g., updating caches). No messages are sent to IRC from this handler.
	desc is a description of the attempted operation for logging purposes.
	"""
	try:
		return func(*args, **kwargs)
	except pylast.WSError as exc:
		lastfm_logger.warning('Error while {}:'.format(desc))
		error_code = int(exc.get_id())
		if error_code == 2:
			lastfm_logger.exception('This service does not exist:')
		elif error_code == 4:
			lastfm_logger.warning('Could not authenticate: wrong username and/or password.')
		elif error_code == 8:
			lastfm_logger.warning('Something else went wrong (generic Last.fm error 8).')
		elif error_code == 11:
			lastfm_logger.warning('Last.fm web services appear to be down.')
		elif error_code == 16:
			lastfm_logger.warning('Temporary error - try again.')
		elif error_code == 29:
			lastfm_logger.warning('Too many requests in a short period.')
		else:
			lastfm_logger.exception('Some other Last.fm error:')
		raise LastFMErrorEncountered(exc)
	except pylast.MalformedResponseError as exc:
		lastfm_logger.exception('Got a MalformedResponseError:')
		raise LastFMErrorEncountered(exc)
	except pylast.NetworkError as exc:
		lastfm_logger.exception('Got a NetworkError:')
		raise LastFMErrorEncountered(exc)

def admin_command(func):
	def wrapper(message_namespace):
		if message_namespace.sender_is_owner:
			func(message_namespace)
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Only an owner may use this command.')
			raise pynoisebot.PermissionDenied
	return wrapper

class die_if_error:
	def __init__(self, name):
		self.thread_name = name

	def __call__(self, func):
		def wrapper(*args, **kwargs):
			try:
				func(*args, **kwargs)
			except BaseException as exc:
				die('The {} thread died!'.format(self.thread_name), exc)
		return wrapper

class LastFMUser:
	def __init__(self, nick, cloak, lastfm_username):
		lastfm_logger.debug('in LastFMUser.__init__')
		self.main_nick = nick
		self.nicks = set()
		self.nicks.add(self.main_nick)
		self.main_cloak = cloak
		self.cloaks = set()
		if self.main_cloak is not None:
			self.cloaks.add(self.main_cloak)
		self.lastfm_username = lastfm_username
		self.url = 'http://www.last.fm/user/{}'.format(self.lastfm_username)
		self.file_name = '{}.lastfm'.format(self.main_nick)
		if username == self.lastfm_username and user_can_authenticate:
			self.base_lastfm_user_obj = network.get_authenticated_user()
			self.authenticated = True
		else:
			self.base_lastfm_user_obj = network.get_user(self.lastfm_username)
			self.authenticated = False
		lastfm_logger.debug('About to request user ID...')
		try:
			self.id = self.base_lastfm_user_obj.get_id()
		except pylast.WSError: # username doesn't exist on Last.fm, or at least Last.fm isn't supposed to return an id for a user that doesn't exist
			lastfm_logger.info('pylast.WSError caught in LastFMUser.__init__()')
			self.authenticated = False
			raise
		self.loved_cache = set()
		self.password_hash = None
		if self.authenticated:
			lastfm_logger.info('User {0} ({1}) is authenticated'.format(self.main_nick, self.lastfm_username))
	
	def __repr__(self):
		return '<LastFMUser object for username {}>'.format(self.lastfm_username)

	def __hash__(self):
		return hash(self.id)

	def compare(self, other, limit=None):
		return self.base_lastfm_user_obj.compare_with_user(other, limit)

	def reload_loved(self):
		self.loved_tracks = self.base_lastfm_user_obj.get_loved_tracks(limit=None)
		self._loved_cache = set()
		for x in self.loved_tracks:
			self._loved_cache.add(x.track)
		self.loved_cache = self._loved_cache
		del self._loved_cache

	def np_url(self):
		current_track = self.base_lastfm_user_obj.get_now_playing()
		if not current_track:
			return 'Could not get URL because {user} is not currently playing anything.'.format(user=self.nick)
		else:
			return current_track.get_url()

	def now_playing(self):
		current_track = self.base_lastfm_user_obj.get_now_playing()
		if not current_track:
			return '\x02{user}\x02 is not currently playing anything'.format(user=self.lastfm_username)
		self.current_pylast_artist = current_track.get_artist()
		self.current_artist = _get_artist_obj_from_pylast_artist_obj(self.current_pylast_artist)
		self.current_album = current_track.get_album()
		if self.current_album is None:
			album = ''
		else:
			album = ' from the release {album}'.format(album=self.current_album.title)
		current_duration_ms = datetime.timedelta(milliseconds=current_track.get_duration())
		current_duration_min, current_duration_sec = divmod(current_duration_ms.seconds, 60)
		if current_duration_sec < 10:
			current_duration_sec = '0{}'.format(str(current_duration_sec))
		current_duration = '{0}:{1}'.format(current_duration_min, current_duration_sec)
		current_tags_list = self.current_artist.get_tags()[:4]
		current_tag_name_list = [tag_name.item.name for tag_name in current_tags_list]
		current_tags_str = ', '.join(current_tag_name_list)
		if not current_tags_str:
			current_tags_str = 'no tags found'
		if current_track in self.loved_cache:
			love = ' [♥]'
		else:
			love = ''
		return '\x02{user}\x02 is now playing: {artist} - {song_title}{album}{love} ({tags}) [{length}]'.format(
			bold='', stop='', user=self.lastfm_username, artist=self.current_artist.name, album=album,
			song_title=current_track.title, love=love, length=current_duration, tags=current_tags_str)

	def save(self):
		with open(os.path.join(profile_dir, self.file_name), 'wb') as user_file:
			pickle.dump(self, user_file)
		lastfm_logger.info('Profile for {nick} ({lfm_user}) was successfully saved'.format(nick=self.main_nick, lfm_user=self.lastfm_username))

	def delete_profile(self):
		os.remove(os.path.join(profile_dir, '{}'.format(self.file_name)))	

class BasicLastFMUser(LastFMUser):
	"""
	A basic and limited throwaway version of LastFMUser for unregistered users.
	Most command functions do not need to care whether they have a basic version or a full version.
	"""
	def __init__(self, lastfm_username):
		self.lastfm_username = lastfm_username
		self.nick = lastfm_username
		self.base_lastfm_user_obj = network.get_user(self.lastfm_username)
		self.url = 'http://www.last.fm/user/{}'.format(self.lastfm_username)
		self.loved_cache = frozenset()
		self.authenticated = False
		try:
			self.id = self.base_lastfm_user_obj.get_id()
		except pylast.WSError: # username doesn't exist on Last.fm, or at least Last.fm isn't supposed to return an id for a user that doesn't exist
			lastfm_logger.info('pylast.WSError caught in LastFMUser.__init__()')
			self.authenticated = False
			raise

	def reload_loved(self):
		raise NotRegistered

	def save(self):
		raise NotRegistered

	def delete_profile(self):
		raise NotRegistered

class LastFMArtist:
	"""
	A wrapper around pylast.Artist that caches information such as tags, top tracks, etc..
	"""
	def __init__(self, pylast_artist_obj):
		self.pylast_artist_obj = pylast_artist_obj
		self.name = self.pylast_artist_obj.name
		self.file_name = '{}.artist'.format(self.name)
		self.tags = None
		self.time_of_last_tag_update = None
		artist_cache_update_jobs.put(self)
		self._save()

	def _update_tags(self):
		"""
		Update the internal cache of tags from Last.fm.
		This method gets overall tags (artist.getTopTags), not a user's individually set tags (artist.getTags).
		"""
		_tags = handle_background_lastfm_errors(pylast.Artist.get_top_tags, 'getting top tags for {}'.format(self.name), self.pylast_artist_obj)
		self.tags = _tags
		self.time_of_last_tag_update = time.time()
		self._save()
		lastfm_logger.debug('Updated tags for {}.'.format(self.name))
	
	def get_tags(self):
		if self.tags is None:
			self._update_tags()
		else:
			days_since_last_update = datetime.timedelta(seconds=time.time() - self.time_of_last_tag_update).days
			lastfm_logger.debug('{0} was last updated {1} days ago.'.format(self.name, days_since_last_update))
			if days_since_last_update > 30:
				self._update_tags()
		return self.tags

	def _save(self):
		encoding = sys.getfilesystemencoding()
		try:
			file_name_bytes = self.file_name.encode(encoding)
		except UnicodeEncodeError:
			file_name_bytes = self.file_name.encode(encoding, 'replace')
			lastfm_logger.error('Could not save an artist cache with the correct name ({0}) because it is has characters that are not supported by your system\'s default encoding ({1}). Using Unicode is HIGHLY recommended.'.format(self.name, encoding))
		finally:
			file_name = file_name_bytes.decode(encoding)
		with open(os.path.join(profile_dir, 'artist cache', file_name), 'wb') as user_file:
			pickle.dump(self, user_file)
		lastfm_logger.info('Artist cache for {} was successfully saved'.format(self.name))

def _get_user_list():
	lastfm_user_list = []
	for username in lastfm_usernames.keys():
		lastfm_user_list.append(username)
	return lastfm_user_list

@die_if_error(name='Background artist cache update thread')
def _add_artist_to_cache():
	global artist_cache_is_free
	while alive:
		artist_obj = artist_cache_update_jobs.get()
		lastfm_logger.debug('Adding {} to the artist cache'.format(artist_obj.name))
		artist_cache_is_free.wait()
		artist_cache_is_free.clear()
		cached_artists[artist_obj.name.lower()] = artist_obj
		artist_cache_is_free.set()
		lastfm_logger.debug('Added {} to the artist cache'.format(cached_artists[artist_obj.name.lower()].name))

@admin_command
def print_cloak_list(message_namespace):
	if message_namespace.sender_is_owner:
		lastfm_logger.info(_get_cloak_list())

def _get_cloak_list():
	global user_dicts_are_free
	user_dicts_are_free.wait()
	return [item for sublist in list(lastfm_cloaks.keys()) for item in sublist]

def _get_nick_list():
	global user_dicts_are_free
	user_dicts_are_free.wait()
	return [item for sublist in list(lastfm_nicks.keys()) for item in sublist]

def _get_single_lastfm_user_object_from_message(message_namespace):
	"""
	Common 'get lastfm user object' code for functions that use either the invoker or a single argument to get the user object.
	"""
	args = message_namespace.message.split()
	if len(args) == 0:
		lastfm_user_object = _get_lastfm_user_object_of_invoker(message_namespace)
	elif len(args) == 1:
		lastfm_user_object = _get_lastfm_user_object_from_arg(args[0], message_namespace)
	else:
		raise _InvalidArguments
	return lastfm_user_object

def _get_lastfm_user_object_from_arg(arg, message_namespace):
	"""
	Detects whether an argument is a username or nick and returns the appropriate (Basic)LastFMUser object.
	Exceptions from the Last.fm user object error handler are not handled here.
	"""
	if _is_nick_registered(arg):
		lastfm_logger.debug('Got the arg "{}" and it is a registered nick.'.format(arg))
		return _get_lastfm_user_obj_from_nick(arg)
	else:
		lastfm_logger.debug('Got the arg "{}" and it is NOT a registered nick.'.format(arg))
		user_obj = BasicLastFMUser.__new__(BasicLastFMUser)
		handle_lastfm_user_obj_errors(user_obj, user_obj.__class__.__init__, message_namespace, lastfm_username=arg)
		return user_obj

def _get_lastfm_user_object_of_invoker(message_namespace):
	if _is_cloak_registered(message_namespace.cloak):
		return _get_lastfm_user_obj_from_cloak(message_namespace.cloak)
	elif _is_nick_registered(message_namespace.nick):
		return _get_lastfm_user_obj_from_nick(message_namespace.nick)
	else:
		user_obj = BasicLastFMUser.__new__(BasicLastFMUser)
		handle_lastfm_user_obj_errors(user_obj, user_obj.__class__.__init__, message_namespace, lastfm_username=message_namespace.nick)
		return user_obj

def _get_lastfm_user_obj_from_cloak(cloak):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	for cloak_tuple in lastfm_cloaks.keys():
		if cloak in cloak_tuple:
			return lastfm_cloaks[cloak_tuple]

def _get_lastfm_user_obj_from_nick(nick):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	for nick_tuple in lastfm_nicks.keys():
		if nick.lower() in nick_tuple:
			return lastfm_nicks[nick_tuple]

def _get_lastfm_user_obj_from_lastfm_username(lastfm_username):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	if lastfm_username in lastfm_usernames.keys():
		return lastfm_usernames[lastfm_username]
	else:
		return BasicLastFMUser(lastfm_username)

def _add_to_user_dicts(lfm_user_obj):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	user_dicts_are_free.clear()
	nicks_lower = set()
	for nick in lfm_user_obj.nicks:
		nicks_lower.add(nick.lower())
	lastfm_nicks.update({tuple(nicks_lower):lfm_user_obj})
	lastfm_cloaks.update({tuple(lfm_user_obj.cloaks):lfm_user_obj})
	lastfm_usernames.update({lfm_user_obj.lastfm_username:lfm_user_obj})
	lastfm_username_to_cloaks.update({lfm_user_obj.lastfm_username:tuple(lfm_user_obj.cloaks)})
	lastfm_username_to_nicks.update({lfm_user_obj.lastfm_username:tuple(lfm_user_obj.nicks)})
	user_dicts_are_free.set()

def _remove_from_user_dicts(lfm_user_obj):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	user_dicts_are_free.clear()
	if lfm_user_obj in lastfm_usernames.values():
		nicks = set()
		for nick in lfm_user_obj.nicks:
			nicks.add(nick.lower())
		del lastfm_nicks[tuple(nicks)]
		del lastfm_cloaks[tuple(lfm_user_obj.cloaks)]
		del lastfm_usernames[lfm_user_obj.lastfm_username]
		del lastfm_username_to_cloaks[lfm_user_obj.lastfm_username]
		del lastfm_username_to_nicks[lfm_user_obj.lastfm_username]
		user_dicts_are_free.set()
		return True
	else:
		user_dicts_are_free.set()
		return False

def _get_artist_obj_from_message(message_namespace):
	if not message_namespace.message:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'No artist specified.')
		raise pynoisebot.InvalidArguments
	given_artist_name = message_namespace.message
	return _get_artist_obj_from_artist_name(message_namespace, given_artist_name)

def _get_artist_obj_from_pylast_artist_obj(pylast_artist_obj):
	"""
	Return a LastFMArtist object from the cache or make one, add it to the cache and return it.
	"""
	artist_cache_is_free.wait()
	try:
		artist_obj = cached_artists[pylast_artist_obj.name.lower()]
		lastfm_logger.debug('{} is cached.'.format(artist_obj.name))
	except KeyError:
		artist_obj = LastFMArtist(pylast_artist_obj)
		lastfm_logger.debug('{} is not cached.'.format(artist_obj.name))
	return artist_obj

def _get_artist_obj_from_artist_name(message_namespace, given_artist_name, try_count=0):
	"""
	Don't send PRIVMSGs/NOTICEs if message_namespace is None (e.g., called from a LastFMUser method).
	"""
	global artist_cache_is_free
	artist_cache_is_free.wait()
	try:
		artist_obj = cached_artists[given_artist_name.lower()]
		lastfm_logger.debug('{} is cached.'.format(given_artist_name))
	except KeyError:
		lastfm_logger.debug('{} is NOT cached.'.format(given_artist_name))
		artist_search = pylast.ArtistSearch.__new__(pylast.ArtistSearch)
		try:
			handle_background_lastfm_errors(pylast.ArtistSearch.__init__, 'searching for {}'.format(given_artist_name), artist_search, given_artist_name, network)
		except LastFMErrorEncountered as lfm_error:
			try:
				raise lfm_error.exc
			except pylast.WSError as exc:
				if message_namespace is not None:
					send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Couldn\'t search for {0} - {1}'.format(given_artist_name, exc.details))
			except pylast.MalformedResponseError:
				if try_count == 0:
					_get_artist_obj_from_artist_name(message_namespace, try_count=try_count + 1)
				else:
					if message_namespace is not None:
						send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Couldn\'t search for {0} - Last.fm sent a malformed response.'.format(given_artist_name))
			except pylast.NetworkError:
				if message_namespace is not None:
					send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Couldn\'t search for {} - Last.fm web services appear to be down.'.format(given_artist_name))
			finally:
				raise lfm_error
		try:
			pylast_artist_obj = artist_search.get_next_page()[0]
		except IndexError:
			if message_namespace is not None:
				send_privmsg(message_namespace.config_network_name, message_namespace.source, 'The artist {} does not exist on Last.fm.'.format(given_artist_name))
			raise pynoisebot.InvalidArguments
		else:
			artist_obj = LastFMArtist(pylast_artist_obj)
			artist_cache_update_jobs.put(artist_obj)
	lastfm_logger.debug(artist_obj)
	return artist_obj

@admin_command
def print_user_dicts(message_namespace):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	print('nicks to object:')
	pprint.pprint(lastfm_nicks)
	print('cloaks to object:')
	pprint.pprint(lastfm_cloaks)
	print('username to object:')
	pprint.pprint(lastfm_usernames)
	print('username to cloaks:')
	pprint.pprint(lastfm_username_to_cloaks)
	print('username to nicks:')
	pprint.pprint(lastfm_username_to_nicks)
print_user_dicts.help_message = 'Prints user information to the console. Only an owner may use this command.'
print_user_dicts.usage_message = ''

def load_users():
	"""
	Config todo: make option to make module fail if one/all files can't be loaded.
	"""
	user_profile_filenames = glob.glob(os.path.join(profile_dir, '*.lastfm'))
	files_not_loaded = set()
	for profile_filename in user_profile_filenames:
		lastfm_logger.debug('loading {}'.format(profile_filename))
		with open(profile_filename, 'rb') as open_file:
			try:
				lastfm_user_obj = pickle.load(open_file)
			except EOFError:
				lastfm_logger.warning('{} is an empty file!'.format(profile_filename))
				files_not_loaded.add(profile_filename)
			except pickle.UnpicklingError:
				traceback.print_exception(*sys.exc_info())
				files_not_loaded.add(profile_filename)
			else:
				try:
					lastfm_user_obj.password_hash = lastfm_user_obj.password_hash
				except AttributeError:
					lastfm_user_obj.password_hash = None
				_add_to_user_dicts(lastfm_user_obj)
				lastfm_logger.info('{nick} ({username}) loaded.'.format(nick=lastfm_user_obj.main_nick, username=lastfm_user_obj.lastfm_username))
	lastfm_logger.info('All users loaded.')
	if files_not_loaded:
		for file_not_loaded in files_not_loaded:
			lastfm_logger.warning('{} could not be loaded.'.format(file_not_loaded))
		return False
	else:
		return True

def load_artist_caches():
	global artist_cache_is_free
	artist_obj_filenames = glob.glob(os.path.join(artist_cache_dir, '*.artist'))
	files_not_loaded = set()
	artist_cache_is_free.wait()
	artist_cache_is_free.clear()
	for artist_obj_filename in artist_obj_filenames:
		lastfm_logger.debug('loading {}'.format(artist_obj_filename))
		with open(artist_obj_filename, 'rb') as open_file:
			try:
				artist_obj = pickle.load(open_file)
			except EOFError:
				lastfm_logger.warning('{} is an empty file!'.format(artist_obj_filename))
				files_not_loaded.add(artist_obj_filename)
			except pickle.UnpicklingError:
				traceback.print_exception(*sys.exc_info())
				files_not_loaded.add(artist_obj_filename)
			else:
				cached_artists[artist_obj.name.lower()] = artist_obj
				lastfm_logger.info('{} artist cache loaded.'.format(artist_obj.name))
	artist_cache_is_free.set()
	lastfm_logger.info('All artist caches loaded.')
	if files_not_loaded:
		for file_not_loaded in files_not_loaded:
			lastfm_logger.warning('{} could not be loaded.'.format(file_not_loaded))
		return False
	else:
		return True

def _load_user(nick):
	with open(os.path.join(profile_dir, '{}.lastfm'.format(nick)), 'rb') as f:
		_add_to_user_dicts(pickle.load(f))

def _is_nick_registered(nick):
	nick_list = _get_nick_list()
	if nick.lower() in nick_list:
		return True
	else:
		return False

def _is_cloak_registered(cloak):
	return cloak in _get_cloak_list()

def _is_username_registered(username):
	global user_dicts_are_free
	user_dicts_are_free.wait()
	return username in lastfm_usernames.keys()

def _get_password_hash(password):
	password_hash = hashlib.sha512(password.encode('unicode_escape')).hexdigest()
	return password_hash

@admin_command
def is_nick_registered(message_namespace):
	lastfm_logger.info('in is_nick_registered')
	nick = message_namespace.message
	if _is_nick_registered(nick):
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'User {} is registered.'.format(nick))
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'User {} is not registered'.format(nick))
is_nick_registered.help_message = 'Displays the registration status of a given nick. Only an owner may use this command.'
is_nick_registered.usage_message = ' <nick>'

@admin_command
def reload_users(message_namespace):
	load_users()
	send_notice(message_namespace.config_network_name, message_namespace.nick, 'All users have been reloaded.')
reload_users.help_message = 'Reloads the loved tracks caches for all registered users. Only an owner may use this command.'
reload_users.usage_message = ''

@die_if_error(name="Reload all loved tracks thread (startup)")
def _reload_all_loved():
	global user_dicts_are_free
	user_dicts_are_free.wait()
	user_dicts_are_free.clear()
	lastfm_user_objs = frozenset(lastfm_usernames.values())
	user_dicts_are_free.set()
	for lastfm_user_obj in lastfm_user_objs:
		try:
			handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.reload_loved, message_namespace=None)
		except LastFMErrorEncountered:
			pass # no reason to stop and the handler logs the error already
	lastfm_logger.debug('The caches of loved tracks for all registered users have been updated.')

def _process_associate_args(message_namespace):
	if message_namespace.message:
		cloak = None
		args = message_namespace.message.split()
		lastfm_logger.debug(args)
		while args:
			arg1 = args.pop(0)
			if arg1 == '-u':
				username = args.pop(0)
			elif arg1 == '-c':
				cloak = args.pop(0)
			elif arg1 == '-p':
				password = args.pop(0)
			else:
				send_notice(message_namespace.config_network_name, message_namespace.nick, 'Invalid arguments.')
				send_notice(message_namespace.config_network_name, message_namespace.nick, 'Usage: associate [-u <lastfm username>] [-c <cloak>] [-p <password>]')
				return
	else:
		cloak = message_namespace.cloak
	if not cloak:
		cloak = message_namespace.cloak
	try:
		lastfm_user_obj = lastfm_usernames[username]
	except NameError:
		lastfm_user_obj = _get_lastfm_user_obj_from_nick(message_namespace.nick)
		if lastfm_user_obj is None:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Your nick is not registered. Try specifying your Last.fm username.')
			raise NotRegistered
	except KeyError:
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'The username {} is not registered.'.format(username))
		raise NotRegistered
	try:
		password_hash = _get_password_hash(password)
	except NameError:
		password_hash = None
	if not message_namespace.sender_is_owner:
		if not (password_hash == lastfm_user_obj.password_hash or lastfm_user_obj.password_hash is None):
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Incorrect password.')
			raise pynoisebot.PermissionDenied
	return (cloak, lastfm_user_obj)

def associate(message_namespace):
	cloak, lastfm_user_obj = _process_associate_args(message_namespace)
	_remove_from_user_dicts(lastfm_user_obj)
	lastfm_user_obj.nicks.add(message_namespace.nick)
	lastfm_user_obj.cloaks.add(cloak)
	_add_to_user_dicts(lastfm_user_obj)
	lastfm_user_obj.save()
	send_privmsg(message_namespace.config_network_name, message_namespace.source, 'The cloak {0} is now associated with the Last.fm username {1}'.format(cloak, lastfm_user_obj.lastfm_username))
associate.help_message = 'Associates your current nick and cloak (or the cloak specified) with your username (or the specified Last.fm username). If the username is protected, the password field is required.'
associate.usage_message = ' [-u <username>] [-c <cloak>] [-p <password>]'

def disassociate(message_namespace):
	cloak, lastfm_user_obj = _process_associate_args(message_namespace)
	_remove_from_user_dicts(lastfm_user_obj)
	try:
		lastfm_user_obj.cloaks.remove(cloak)
	except KeyError:
		cloak_existed = False
	else:
		cloak_existed = True
	_add_to_user_dicts(lastfm_user_obj)
	lastfm_user_obj.save()
	if cloak_existed:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'The cloak {0} is no longer associated with the Last.fm username {1}'.format(cloak, lastfm_user_obj.lastfm_username))
	else:
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'The cloak {} was not associated with your username.'.format(cloak))
disassociate.help_message = 'Disassociates your current nick and cloak (or the cloak specified) from your username (or the specified Last.fm username). If the username is protected, the password field is required.'
disassociate.usage_message = ' [-u <username>] [-c <cloak>] [-p <password>]'


def set_lastfm_password(message_namespace):
	lastfm_user_obj = _get_lastfm_user_obj_from_cloak(message_namespace.cloak)
	if lastfm_user_obj is None:
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'Your cloak is not registered.')
		raise NotRegistered
	if not message_namespace.message:
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'No password specified.')
		raise pynoisebot.InvalidArguments
	args = message_namespace.message.split()
	if len(args) == 1:
		password_hash = _get_password_hash(args[0])
		if lastfm_user_obj.password_hash is None: # set new password
			lastfm_user_obj.password_hash = password_hash
		else: # remove current password
			if password_hash == lastfm_user_obj.password_hash:
				lastfm_user_obj.password_hash = None
			else:
				send_notice(message_namespace.config_network_name, message_namespace.nick, 'Incorrect password.')
				raise pynoisebot.PermissionDenied
	else:
		new_password_hash = _get_password_hash(args[0])
		old_password_hash = _get_password_hash(args[1])
		if old_password_hash == lastfm_user_obj.password_hash:
			lastfm_user_obj.password_hash = new_password_hash
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Incorrect password.')
			raise pynoisebot.PermissionDenied
	lastfm_user_obj.save()
		
set_lastfm_password.help_message = 'Sets or changes the password required to associate cloaks. If a new password is not specified, the password is cleared.'
set_lastfm_password.usage_message = ' [<new password>] [<old password>]'

def register(message_namespace, admin=False):
	if not admin:
		if message_namespace.cloak in lastfm_cloaks.keys():
			lastfm_user_obj = _get_lastfm_user_obj_from_cloak(message_namespace.cloak)
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'You are already registered for the Last.fm username {}.'.format(lastfm_user_obj.lastfm_username))
			return
	args = message_namespace.message.split()
	if not admin:
		if len(args) == 0:
			lastfm_username = message_namespace.nick
		elif len(args) == 1:
			lastfm_username = message_namespace.message
		else:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Too many arguments. Usage: {}register [<Last.fm username>]')
			return
		nick = message_namespace.nick
		cloak = message_namespace.cloak
	else:
		if len(args) != 2:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Usage: {}register2 <nick> <Last.fm username>')
			return
		nick = args[0]
		lastfm_username = args[1]
		cloak = None
	if lastfm_username in _get_user_list():
		regged_nick = _get_lastfm_user_obj_from_lastfm_username(lastfm_username).main_nick
		send_privmsg(message_namespace.config_network_name, 'This username is already registered for {}.'.format(regged_nick))
		return
	lastfm_logger.info('nick: {0} | cloak: {1} | last.fm username: {2}'.format(nick, cloak, lastfm_username))
	lastfm_user_obj = LastFMUser.__new__(LastFMUser)
	handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.__init__, message_namespace, nick=nick, cloak=cloak, lastfm_username=lastfm_username)
	send_notice(message_namespace.config_network_name, message_namespace.nick, 'Caching loved tracks...')
	handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.reload_loved, message_namespace)
	_add_to_user_dicts(lastfm_user_obj)
	send_privmsg(message_namespace.config_network_name, message_namespace.source, '{0} was successfully registered for the Last.fm user {1}!'.format(lastfm_user_obj.main_nick, lastfm_user_obj.lastfm_username))
	if lastfm_user_obj.main_cloak is not None:
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'Your cloak ({0}) is also associated with the Last.fm username {1}'.format(lastfm_user_obj.main_cloak, lastfm_user_obj.lastfm_username))
	try:
		lastfm_user_obj.save()
	except:
		lastfm_logger.exception('Could not save profile for {0} ({1}):'.format(lastfm_user_obj.main_nick, lastfm_user_obj.lastfm_username))
		send_notice(message_namespace.config_network_name, message_namespace.nick, 'Your profile couldn\'t be saved. Contact the bot\'s owner.')
register.help_message = 'Registers your Last.fm username with this module to enable extra features and associates it with your nick and cloak.'
register.usage_message = ' [<Last.fm username>]'

@admin_command
def register2(message_namespace):
	"""
	.register2 <nick> <lastfm_username>
	"""
	register(message_namespace, admin=True)
register2.help_message = 'Registers the given Last.fm username and associates it with the given nick. Only an owner may use this command.'
register2.usage_message = ' <nick> [<Last.fm username>]'

def user_info(message_namespace):
	if message_namespace.message:
		if message_namespace.sender_is_owner:
			if _is_nick_registered(message_namespace.message):
				lastfm_user_obj = _get_lastfm_user_obj_from_nick(message_namespace.message)
			else:
				send_notice(message_namespace.config_network_name, message_namespace.nick, 'The nick {} is not registered.'.format(message_namespace.message))
				raise NotRegistered
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Only an owner may request information on other users.')
			raise pynoisebot.PermissionDenied
	elif _is_nick_registered(message_namespace.nick):
		lastfm_user_obj = _get_lastfm_user_obj_from_nick(message_namespace.nick)
	else:
		send_privmsg(message_namespace.config_network_name, 'You are not registered.')
		raise NotRegistered
	send_privmsg(message_namespace.config_network_name, message_namespace.nick, 'Associated nicks: {}'.format(' '.join(lastfm_user_obj.nicks)))
	send_privmsg(message_namespace.config_network_name, message_namespace.nick, 'Associated cloaks: {}'.format(' '.join(lastfm_user_obj.cloaks)))
	send_privmsg(message_namespace.config_network_name, message_namespace.nick, 'Last.fm username: {}'.format(lastfm_user_obj.lastfm_username))
	send_privmsg(message_namespace.config_network_name, message_namespace.nick, 'Password hash: {}'.format(lastfm_user_obj.password_hash))
user_info.help_message = 'Displays various properties of a your Last.fm user object. Only an owner may request information on other users.'
user_info.usage_message = ' [<registered nick>]'

@admin_command
def unregister(message_namespace):
	"""
	Unregister a nick (not a Last.fm username!)
	"""
	if not message_namespace.message:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'No user specified.')
	else:
		args = message_namespace.message.split()
		if len(args) > 1:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Too many arguments. Usage: {}unregister <registered nick>')
			return
		nick = args[0]
		lastfm_user_obj = _get_lastfm_user_obj_from_nick(nick)
		if lastfm_user_obj:
			_remove_from_user_dicts(lastfm_user_obj)
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, '{} is not registered.'.format(nick))
			return
		try:
			lastfm_user_obj.delete_profile()
		except (OSError, IOError) as exc:
			lastfm_logger.exception('Something went wrong trying to delete the user profile for {}:'.format(lastfm_user_obj.lastfm_username))
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Profile could not be deleted. Check paths.')
			send_notice(message_namespace.config_network_name, message_namespace.nick, '{}'.format(exc))
			return
		else:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, '{}\'s profile was successfully unregistered and deleted.'.format(lastfm_user_obj.main_nick))
unregister.help_message = 'Unregisters a registered nick. Only an owner may use this command.'
unregister.help_message = ' <registered nick>'

def now_playing(message_namespace):
	lastfm_user_obj = _get_single_lastfm_user_object_from_message(message_namespace)
	np_message = handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.now_playing, message_namespace)
	send_privmsg(message_namespace.config_network_name, message_namespace.source, np_message)
now_playing.help_message = 'Displays what you are playing or what the specified user is playing.'
now_playing.usage_message = ' [<Last.fm username or registered nick>]'

def np_url(message_namespace):
	lastfm_user_obj = _get_single_lastfm_user_object_from_message(message_namespace)
	url = handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.np_url, message_namespace)
	send_privmsg(message_namespace.config_network_name, message_namespace.source, url)
np_url.help_message = 'Displays the Last.fm URL for what you are playing or what the specified user is playing.'
np_url.usage_message = ' [<Last.fm username or registered nick>]'

def reload_loved(message_namespace):
	args = message_namespace.message.split()
	if len(args) == 0:
		lastfm_user_obj = _get_lastfm_user_object_of_invoker(message_namespace)
	elif len(args) == 1:
		if message_namespace.sender_is_owner:
			lastfm_user_obj = _get_lastfm_user_object_of_invoker(message_namespace)
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Only an owner may use this command with an argument.')
			raise pynoisebot.PermissionDenied
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Too many arguments.')
		raise pynoisebot.InvalidArguments
	try:
		handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.reload_loved, message_namespace)
		handle_lastfm_user_obj_errors(lastfm_user_obj, lastfm_user_obj.__class__.save, message_namespace)
	except NotImplemented:
		if message_namespace is not None:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Only a registered username can have a cache of loved tracks.')
	else:
		if message_namespace is not None:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Your loved tracks cache has been updated.')
reload_loved.help_message = 'Updates your cache of loved tracks. You must be registered to have a cache of loved tracks.'
reload_loved.usage_message = ''

def compare(message_namespace):
	args = message_namespace.message.split()
	if len(args) == 0:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'No user given to compare to.')
		raise pynoisebot.InvalidArguments
	elif len(args) == 1:
		user1 = _get_lastfm_user_object_of_invoker(message_namespace)
		user2 = _get_lastfm_user_object_from_arg(args[0], message_namespace)
	elif len(args) == 2:
		[lastfm_logger.debug(arg) for arg in args]
		user1, user2 = [_get_lastfm_user_object_from_arg(arg, message_namespace) for arg in args]
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Can only compare two users.')
		raise pynoisebot.InvalidArguments
	compare_result = handle_lastfm_user_obj_errors(user1, user1.__class__.compare, message_namespace, other=user2.lastfm_username, limit=8)
	score, common_artists = compare_result
	score_percent = round(float(score), 4) * 100
	if common_artists:
		artist_name_list = []
		for artist in common_artists:
			artist_name = artist.name
			artist_name_list.append(artist_name)
		common_artist_string = ' Common artists include: {}'.format(', '.join(artist_name_list))
	else:
		common_artist_string = ''
	send_privmsg(message_namespace.config_network_name, message_namespace.source, '\x02{0}\x02 and \x02{1}\x02 are {2}% compatible.{3}'.format(user1.lastfm_username, user2.lastfm_username, score_percent, common_artist_string))
	
compare.help_message = 'Compares you to another Last.fm user or two users to each other and displays compatibility.'
compare.usage_message = ' <Last.fm username or registered nick> [<another username or nick>]'

def get_artist_tags(message_namespace):
	artist_obj = _get_artist_obj_from_message(message_namespace)
	tags = artist_obj.get_tags()
	lastfm_logger.debug(tags.__class__)
	tag_strings = ['{0} ({1})'.format(tag.item.name, tag.weight) for tag in tags][:10]
	if tag_strings:
		message = 'Top tags for \x02{0}\x02: {1}'.format(artist_obj.name, ', '.join(tag_strings))
	else:
		message = '{} is not tagged.'.format(artist_obj.name)
	send_privmsg(message_namespace.config_network_name, message_namespace.source, message)

@admin_command
def update_artist_cache(message_namespace, try_count=0):
	artist_obj = _get_artist_obj_from_message(message_namespace)
	try:
		handle_background_lastfm_errors(LastFMArtist._update_tags, 'updating tags for {}'.format(artist_obj.name), artist_obj)
	except LastFMErrorEncountered:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Could not update tag cache for {}. See logs.'.format(artist_obj.name))
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Tag cache for {} was successfully updated.'.format(artist_obj.name))

def lastfm_help(message_namespace):
	global module_name
	trigger = connected_servers[message_namespace.config_network_name].trigger
	if not message_namespace.message:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'The Last.fm module provides an interface to several Last.fm functions that provide information such as what a user is currently playing or how compatible two users\' taste in music is.')
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Type {0}commands {1} to see available commands.'.format(trigger, module_name))
		return
	try:
		command_func = commands_to_functions[message_namespace.message]
	except KeyError:
		lastfm_logger.error('Got a help request for an unregistered command... ({})'.format(message_namespace.message))
		return
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, command_func.help_message)
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{0}{1}{2}'.format(trigger, message_namespace.message, command_func.usage_message))

def send_privmsg(server_name, dest, message):
	outgoing_message_dicts.put({'action':'irc_message', 'server':connected_servers[server_name].config_network_name, 'message_type':'privmsg', 'args':dest, 'message':message})

def send_notice(server_name, dest, message):
	outgoing_message_dicts.put({'action':'irc_message', 'server':connected_servers[server_name].config_network_name, 'message_type':'notice', 'args':dest, 'message':message})

json_encoder = pynoisebot.json_encoder
json_decoder = pynoisebot.json_decoder

@die_if_error(name='Receive Messages From Bot')
def recv_messages_from_bot(connection):
	while alive:
		incoming_json = connection.recv()
		incoming_messages.put(json_decoder.decode(incoming_json))

@die_if_error(name='Send Messages To Bot')
def send_messages_to_bot(connection):
	while alive:
		outgoing_message_dict = outgoing_message_dicts.get()
		outgoing_message_json = json_encoder.encode(outgoing_message_dict)
		pynoisebot.send_message(connection, outgoing_message_json)
		outgoing_message_dicts.task_done()

@die_if_error(name='Incoming Messages')
def process_incoming_messages():
	while alive:
		incoming_message = types.SimpleNamespace(**incoming_messages.get())
		if incoming_message.action == 'irc_message':
			try:
				command_func = commands_to_functions[incoming_message.command]
			except KeyError:
				lastfm_logger.error('Got a command that isn\'t in commands_to_functions: {}'.format(incoming_message.command))
			else:
				lastfm_logger.info('Got a command: {}'.format(incoming_message.command))
				threading.Thread(target=process_command, args=(command_func, incoming_message), daemon=False).start()
		elif incoming_message.action in ('error', 'die'):
			lastfm_logger.error('Error message from bot: {}'.format(incoming_message.reason))
			die()
		elif incoming_message.action == 'ping':
			pynoisebot.ping_bot(connection)
		elif incoming_message.action == 'server_info':
			if incoming_message.config_network_name in connected_servers.keys():
				connected_servers[incoming_message.config_network_name].update(incoming_message)
			else:
				connected_servers[incoming_message.config_network_name] = pynoisebot.ConnectedServer(incoming_message)
		elif incoming_message.action == 'channel_info':
			pass
		elif incoming_message.action == 'module_info':
			global module_name
			module_name = incoming_message.module_name
			lastfm_logger.info('Module name is {}'.format(module_name))
		else:
			lastfm_logger.error('Ignored invalid message from bot: {}'.format(incoming_message))
		incoming_messages.task_done()

commands_to_functions = {'np':now_playing,
						'nowplaying':now_playing,
						'npurl':np_url,
						'compare':compare,
						'tags':get_artist_tags,
						'register':register,
						'reloadloved':reload_loved,
						'associate':associate,
						'disassociate':disassociate,
						'setlastfmpassword':set_lastfm_password,
						'help':lastfm_help,
						'register2':register2,
						'unregister':unregister,
						'userinfo':user_info,
						'printuserdicts':print_user_dicts,
						'isnickregged':is_nick_registered,
						'updateartistcache':update_artist_cache}

def process_command(func, message_namespace):
	try:
		func(message_namespace)
	except (LastFMErrorEncountered, NotRegistered, pynoisebot.PermissionDenied, pynoisebot.InvalidArguments):
		pass # the errors are already handled - these exceptions abort the call by propagating up to this point

def die(reason, exc):
	global alive
	global connection
	pynoisebot.send_die_message(connection, reason)
	alive = False
	connection.close()
	raise exc

alive = True
connection = pynoisebot.establish_connection(('localhost', 20153))
recv_messages_from_bot_thread = threading.Thread(target=recv_messages_from_bot, args=(connection,), daemon=True)
send_messages_to_bot_thread = threading.Thread(target=send_messages_to_bot, args=(connection,), daemon=True)
incoming_messages = queue.Queue()
outgoing_message_dicts = queue.Queue()
commands_to_register = frozenset(commands_to_functions.keys()).difference({'help'})
pynoisebot.register_module(conn=connection, recv_events=False, recv_commands=True, recv_message_types=(), commands=tuple(commands_to_register))
user_dicts_are_free = threading.Event()
user_dicts_are_free.set()
artist_cache_is_free = threading.Event()
artist_cache_is_free.set()
recv_messages_from_bot_thread.start()
send_messages_to_bot_thread.start()
connected_servers = {}

default_lastfm_conf_dir = os.path.join(os.getcwd(), 'modules', 'data')
default_lastfm_conf_file = 'lastfm.json'

try:
	os.mkdir(default_lastfm_conf_dir)
except FileExistsError:
	conf_dir_log_msg = (logging.INFO, 'Using existing directory {}.'.format(default_lastfm_conf_dir))
else:
	conf_dir_log_msg = (logging.INFO, 'New config directory created: {}.'.format(default_lastfm_conf_dir))

with open(os.path.join(default_lastfm_conf_dir, default_lastfm_conf_file)) as conf_file:
	config = json_decoder.decode(conf_file.read())
lastfm_auth = config['auth']
log_conf = config['logging']
log_str_to_level = {'debug':logging.DEBUG,
					'info':logging.INFO,
					'warning':logging.WARNING,
					'error':logging.ERROR,
					'critical':logging.CRITICAL,
					'fatal':logging.FATAL}
verbosity_str = log_conf['verbosity']
log_verbosity = log_str_to_level[verbosity_str]

lastfm_logger = logging.getLogger(__name__)
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler(os.path.join(default_lastfm_conf_dir, 'lastfm.log'), encoding='utf-8')
console_formatter = logging.Formatter('{asctime} - {module} - {funcName} - line {lineno} - {levelname} - {message}', style='{')
file_formatter = logging.Formatter('{asctime} - {module} - {funcName} - line {lineno} - {levelname} - {message}', style='{')
console_handler.setFormatter(console_formatter)
file_handler.setFormatter(file_formatter)
lastfm_logger.addHandler(console_handler)
lastfm_logger.addHandler(file_handler)

lastfm_logger.setLevel(log_verbosity)
lastfm_logger.log(*conf_dir_log_msg)

api_key = lastfm_auth['apikey']
api_secret = lastfm_auth['apisecret']
username = lastfm_auth['username']
password_hash = pylast.md5(lastfm_auth['userpass'])

if api_secret and username and password_hash:
	user_can_authenticate = True
else:
	user_can_authenticate = False

def get_lastfm_network(api_key, api_secret, username, password_hash):
	return pylast.LastFMNetwork(api_key=api_key, api_secret=api_secret, username=username, password_hash=password_hash)

try:
	network = get_lastfm_network(api_key, api_secret, username, password_hash)
except pylast.WSError as exc:
	error_code = int(exc.get_id())
	if error_code in (4, 13):
		network = get_lastfm_network(api_key, api_secret=None, username=None, password_hash=None)
	else:
		die('Could not connect to Last.fm.', exc)
except Exception as exc:
	die('Could not connect to Last.fm.', exc)

lastfm_username_to_cloaks = {} # keys are registered last.fm usernames, values are lists of the associated cloaks
lastfm_username_to_nicks = {} # keys are registered last.fm usernames, values are lists of the associated nicks
lastfm_cloaks = {} # keys are tuples of registered cloaks (a last.fm username can have more than one cloak), values are the associated LastFMUser objects
lastfm_nicks = {} # keys are tuples of registered nicks (a last.fm username can have more than one nick), values are the associated LastFMUser objects
lastfm_usernames = {} # keys are the last.fm usernames, values are the associated LastFMUser objects
cached_artists = {} # keys are the artist names (lowercase), values are the associated LastFMArtist objects
artist_cache_update_jobs = queue.Queue()
profile_dir = os.path.join(default_lastfm_conf_dir, 'lastfm')
artist_cache_dir = os.path.join(profile_dir, 'artist cache')
try:
	os.mkdir(profile_dir)
except FileExistsError:
	lastfm_logger.debug('Using existing profile directory {}.'.format(profile_dir))
try:
	os.mkdir(artist_cache_dir)
except FileExistsError:
	lastfm_logger.debug('Using existing artist cache directory {}.'.format(artist_cache_dir))

load_users()
load_artist_caches()
lastfm_logger.debug('start reload thread')
threading.Thread(target=_reload_all_loved, daemon=False).start()
lastfm_logger.debug('started reload thread')
threading.Thread(target=_add_artist_to_cache, daemon=False).start()
lastfm_logger.debug('started add artist to cache thread')
process_incoming_messages()
